/****

    activequant - activestocks.eu

    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 2 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License along
    with this program; if not, write to the Free Software Foundation, Inc.,
    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

	
	contact  : contact@activestocks.eu
    homepage : http://www.activestocks.eu

****/
package org.activequant.core.util;

import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.ListIterator;
import java.util.Set;
import java.util.TreeSet;

import org.activequant.core.domainmodel.SeriesSpecification;
import org.activequant.core.domainmodel.data.Candle;
import org.activequant.core.domainmodel.data.CandleSeries;
import org.activequant.core.domainmodel.data.MarketDataEntity;
import org.activequant.core.domainmodel.data.Quote;
import org.activequant.core.domainmodel.data.QuoteSeries;
import org.activequant.core.domainmodel.data.TimeSeries;
import org.activequant.core.domainmodel.data.TradeIndication;
import org.activequant.core.domainmodel.data.TradeIndicationSeries;
import org.activequant.core.types.TimeStamp;
import org.activequant.core.types.Tuple;
import org.activequant.util.exceptions.InvalidDateOrderException;
import org.activequant.util.exceptions.ValueNotFoundException;


/**
 * @TODO desc<br>
 * <br>
 * <b>History:</b><br>
 *  - [03.05.2007] Created (Erik Nijkamp)<br>
 *  - [10.06.2007] added merge (Ulrich Staudinger)<br>
 *  - [15.06.2007] Polished merge() (Erik Nijkamp)<br>
 *  - [25.01.2008] Generic version + fixes (Erik Nijkamp)<br>
 *  - [02.03.2008] Moved dirty sublist code to TimeSeriesUtils (Erik Nijkamp)<br>
 *  - [02.03.2008] Fixed cloneSeries() (Erik Nijkamp)<br>
 *  
 *  @author Erik Nijkamp
 *  @author Ulrich Staudinger
 */
public class TimeSeriesUtils {
	/**
	 * Returns a <code>Candle.class</code>, <code>Quote.class</code> or <code>TradeIndication.class</code> depending on whether the given series(T) is
	 * a CandleSeries, QuoteSeries or TradeIndicationSeries.
	 * @param <U>
	 * @param <T>
	 * @param series
	 * @return
	 */
    @SuppressWarnings("unchecked")
	public static <U extends MarketDataEntity<U>, T extends TimeSeries<U>>
	Class<U> getEntityClass(T series) {
    	if(CandleSeries.class.isAssignableFrom(series.getClass())) {
    		return (Class<U>) (Class<?>) Candle.class;
		} else if(QuoteSeries.class.isAssignableFrom(series.getClass())) {
    		return (Class<U>) (Class<?>) Quote.class;
		} else if(TradeIndicationSeries.class.isAssignableFrom(series.getClass())) {
    		return (Class<U>) (Class<?>) TradeIndication.class;
		}
		throw new IllegalArgumentException("Unknown series");
    }
    /**
     * sets the openPrice(double)'s, highPrice(double)'s, lowPrice(double)'s and closePrice(double)'s of each of the Candle elements in the given
	 * series(T) (must be CandleSeries) with the given values(double[][])
     * @param <T>
     * @param series
     * @param values
     */
    public static<T extends TimeSeries<?>> 
    void setSeriesValues(T series, double[][] values) {
    	if(series instanceof CandleSeries) {
    		assert(values.length == 4) : "array length != 4";
    		CandleSeries cs = (CandleSeries) series;    		
    		cs.setOpens(values[0]);
    		cs.setHighs(values[1]);
    		cs.setLows(values[2]);
    		cs.setCloses(values[3]);
		}
		throw new IllegalArgumentException("Unknown series");
    }
	/**
	 * Returns a double[][] with the openPrice(double)'s, highPrice(double)'s, lowPrice(double)'s and closePrice(double)'s of each of the Candle elements in the given
	 * series(T) (must be CandleSeries)
	 * @param <T>
	 * @param series
	 * @return
	 */
	public static <T extends TimeSeries<?>> 
	double[][] getSeriesValues(T series) {
		if(series instanceof CandleSeries) {
			CandleSeries cs = (CandleSeries) series;
			return new double[][] { 
					cs.getOpens(), 
					cs.getHighs(), 
					cs.getLows(), 
					cs.getCloses() };
		}
		throw new IllegalArgumentException("Unknown series");
	}
	
	/**
	 * returns the relative return series. Omits the first day. 
	 * Example: Closes of candles are : 10, 11, 11
	 * Relative returns are : 0.1, 0.0;
	 * 
	 * @return
	 */
	public static double[] getRelativeReturns(CandleSeries cs) {
		double[] returns = new double[cs.size()-1];
		for(int i=1;i<cs.size();i++){
			Candle c0 = cs.get(i-1);
			Candle c1 = cs.get(i);
			returns[i-1] = (c0.getClosePrice() - c1.getClosePrice()) / c1.getClosePrice();
		}
		return returns;
	}
	/**
	 * Returns closes(List&lt;Tuple&lt;TimeStamp,Double&gt;&gt;) holding the timeStamp-closePrice pairs of each Candle in the given series(CandleSeries)
	 * @param series
	 * @return
	 */
    public static List<Tuple<TimeStamp, Double>> getClosesAsTuples(CandleSeries series) {
    	List<Tuple<TimeStamp, Double>> ret = new ArrayList<Tuple<TimeStamp, Double>>();
    	for(Candle candle : series) {
    		Tuple<TimeStamp, Double> t = new Tuple<TimeStamp, Double>();
    		t.setObject1(candle.getTimeStamp());
    		t.setObject2(candle.getClosePrice());
    		ret.add(t);
    	}
    	return ret;
    }
    /**
     * clones the given in(T...) (where T extends TimeSeries)
     * @param <U>
     * @param <T>
     * @param clazz
     * @param in
     * @return
     */
	@SuppressWarnings("unchecked")
	public static <U extends MarketDataEntity<U>, T extends TimeSeries<U>> 
	T[] cloneSeries(Class<U> clazz, T... in) {
		T[] out = (T[]) Array.newInstance(in[0].getClass(), in.length);
		for (int i = 0; i < in.length; i++) {
			out[i] = (T) in[i].clone();
		}
		return out;
	}
	/**
	 * clears the elements of each TimeSeries in the given series(T...)
	 * @param <U>
	 * @param <T>
	 * @param clazz
	 * @param series
	 */
	public static <U extends MarketDataEntity<U>, T extends TimeSeries<U>> 
	void clearSeries(Class<U> clazz, T... series) {
		for(T timeSeries: series) {
			timeSeries.clear();
		}
	}
	
    /**
     * will align two timeseries. will remove days that are not present in one of the two series. 
     * @param ts1
     * @param ts2
     */
    public static <M extends MarketDataEntity<M>, T extends TimeSeries<M>> 
    void alignTimeSeries(Class<M> clazz, T ts1, T ts2) {
        HashMap<String, Integer> entries = new HashMap<String, Integer>();

        for (M c : ts1) {
            entries.put(c.getTimeStamp().toString(), 1);
        }

        for (M c : ts2) {
            if (entries.containsKey(c.getTimeStamp().toString()))
                entries.put(c.getTimeStamp().toString(), 2);
            else
                entries.put(c.getTimeStamp().toString(), 1);
        }

        List<M> toBeDeleted = new ArrayList<M>();

        // walk over the ts1 and remove all the unknown candles.
        for (M c : ts1) {
            if (entries.get(c.getTimeStamp().toString()) != 2) {
                toBeDeleted.add(c);
            }
        }

        // walk over the ts2 and remove all the unknown candles.
        for (M c : ts2) {
            if (entries.get(c.getTimeStamp().toString()) != 2) {
                toBeDeleted.add(c);
            }
        }

        for (M c : toBeDeleted) {
            ts1.remove(c);
            ts2.remove(c);
        }
    }    
    /**
     * returns a CandleSeries by merging the given series(CandleSeries...) (ordered by TimeStamp - newest at index 0 to oldest at the end).<br/>
     * Its seriesSpecification(SeriesSpecification) is set with the given series[0](CandleSeries)
     * @param series
     * @return
     */
    public static CandleSeries merge(CandleSeries... series) {
    	Set<Candle> set = newSortedSet();
    	for(CandleSeries cs : series) {
    		set.addAll(cs);
    	}
    	SeriesSpecification spec = new SeriesSpecification(series[0].getSeriesSpecification());
    	CandleSeries result = new CandleSeries(spec, set.toArray(new Candle[] {}));    	
		assert(isOrderValid(result));    	
    	return result; 
    }
    /**
     * returns a QuoteSeries by merging the given series(QuoteSeries...) (ordered by TimeStamp - newest at index 0 to oldest at the end).<br/>
     * Its seriesSpecification(SeriesSpecification) is set with the given series[0](QuoteSeries)
     * @param series
     * @return
     */
    public static QuoteSeries merge(QuoteSeries... series) {
    	Set<Quote> set = newSortedSet();
    	for(QuoteSeries cs : series) {
    		set.addAll(cs);
    	}
    	SeriesSpecification spec = new SeriesSpecification(series[0].getSeriesSpecification());
    	QuoteSeries result = new QuoteSeries(spec, set.toArray(new Quote[] {}));    	
		assert(isOrderValid(result));    	
    	return result; 
    }
    /**
     * returns a TradeIndicationSeries by merging the given series(TradeIndicationSeries...) (ordered by TimeStamp - newest at index 0 to oldest at the end).<br/>
     * Its seriesSpecification(SeriesSpecification) is set with the given series[0](TradeIndicationSeries)
     * @param series
     * @return
     */
    public static TradeIndicationSeries merge(TradeIndicationSeries... series) {
    	Set<TradeIndication> set = newSortedSet();
    	for(TradeIndicationSeries cs : series) {
    		set.addAll(cs);
    	}
    	SeriesSpecification spec = new SeriesSpecification(series[0].getSeriesSpecification());
    	TradeIndicationSeries result = new TradeIndicationSeries(spec, set.toArray(new TradeIndication[] {}));    	
		assert(isOrderValid(result));    	
    	return result; 
    }
    /**
     * returns a new TreeSet&lt;T&gt; (where T extends MarketDataEntity) and elements are ordered by their Timestamps
     * @param <T>
     * @return
     */
    private static <T extends MarketDataEntity<T>> Set<T> newSortedSet() {
    	return new TreeSet<T>(new Comparator<T>() {
			public int compare(T c2, T c1) {
				// null-safe (just in case)
				if(c1.getTimeStamp() == null) {
					return c2.getTimeStamp() == null ? 0 : -1;
				} else if(c2.getTimeStamp() == null) {
					return 1;
				}				
				return c1.getTimeStamp().compareTo(c2.getTimeStamp());
			}
    	});
    }
    /**
     * Reverts the given timeSeries(TimeSeries&lt;T&gt;) if it isn't in valid order (latest/newest data at index=0). If that doesn't help either, it throws an AssertionException.<br/>
     * The elements(T extends MarketDataEntity&lt;T&gt;) in the given timeSeries(TimeSeries&lt;T&gt;) should be ordered so that the latest/newest one is at index 0.
     * @param <T>
     * @param timeSeries
     */
	public static <T extends MarketDataEntity<T>> void alignDateOrder(TimeSeries<T> timeSeries) {
		// need to check if we need to reverse the order due to date stuff.
		if (timeSeries.size() > 1 && !isOrderValid(timeSeries)) {
			Collections.reverse(timeSeries);
		}
		assert(isOrderValid(timeSeries));
	}
    
    /**
     * helper method to sort a candle series. 
     * @param ts1
     * @return
     */
    public static <T extends MarketDataEntity<T>> 
    void sort(TimeSeries<T> timeSeries) {
    	Collections.sort(timeSeries);
    	alignDateOrder(timeSeries);
    }
    
	/**
	 * candles[0] newest, candles[n] oldest<br/>
	 * returns whether the timeStamp(TimeStamp)'s of the given entities(MarketDataEntity&lt;?&gt; ...) are ordered from newest(at index=0) to oldest(at index=n)
	 * @param candles
	 * @return
	 */
	public static boolean isOrderValid(MarketDataEntity<?> ... entities) {
		for(int i = 1; i < entities.length; i++) {
			if(entities[i-1].getTimeStamp().isBefore(entities[i].getTimeStamp())) {
				// invalid candle order found. 
				return false;
			}
		}
		return true;
	}
	/**
	 * returns whether the timeStamp(TimeStamp)'s of the given entities(TimeSeries&lt;?&gt; ...) are ordered from newest(at index=0) to oldest(at index=n)
	 * @param <T>
	 * @param entities
	 * @return
	 */
	public static <T extends TimeSeries<?>> boolean isOrderValid(T entities) {
		return isOrderValid(entities.toArray(new MarketDataEntity[] {}));
	}
	/**
	 * Checks whether the timeStamp(TimeStamp)'s of the given entities(MarketDataEntity&lt;?&gt; ...) are ordered from newest(at index=0) to oldest(at index=n).
	 * Otherwise it throws an InvalidDateOrderException.
	 * @param entities
	 * @throws InvalidDateOrderException
	 */
	public static void checkOrder(MarketDataEntity<?> ... entities) throws InvalidDateOrderException {
		if(!isOrderValid(entities)) throw new InvalidDateOrderException();
	}
	/**
	 * Checks whether the timeStamp(TimeStamp)'s of the given entities(List&lt;MarketDataEntity&lt;?&gt;&gt;) are ordered from newest(at index=0) to oldest(at index=n).
	 * Otherwise it throws an InvalidDateOrderException.
	 * @param entities
	 * @throws InvalidDateOrderException
	 */
	public static void checkOrder(List<MarketDataEntity<?>> entities) throws InvalidDateOrderException {
		if(!isOrderValid(entities.toArray(new MarketDataEntity[] {}))) throw new InvalidDateOrderException();
	}
	
	

   
	/**
     * returns the position of the given date within the array.<br/>
     * Returns the feasible position index(int) of the given date(TimeStamp) within the given series(&lt;T extends TimeSeries&lt;U&gt;&gt;) 
     * (assuming it is ordered - latest at index 0, to oldest at the end)
     * @param date
     * @return
     * @throws ValueNotFoundException
     */
	public static <T extends TimeSeries<U>, U extends MarketDataEntity<U>>
	int getFeasibleTimeStampPosition(Class<U> clazz, T series, TimeStamp date) throws ValueNotFoundException {
        ListIterator<U> iter = series.listIterator();
		while (iter.hasNext()) {
			U next = iter.next();
			if (next.getTimeStamp().isEqual(date) || next.getTimeStamp().isBefore(date))
				return iter.previousIndex();
		}
		throw new ValueNotFoundException("Cannot find value for date '" + date + "'.");
	}
	
	public static <T extends TimeSeries<U>, U extends MarketDataEntity<U>>
	U getByFeasibleTimeStamp(Class<U> clazz, T series, TimeStamp date) throws ValueNotFoundException {
		return series.get(getFeasibleTimeStampPosition(clazz, series, date));
	}

	public static <T extends TimeSeries<U>, U extends MarketDataEntity<U>>
	int getLastFeasiblePositionTimeStamp(Class<U> clazz, T series, TimeStamp date) throws ValueNotFoundException {
		ListIterator<U> iter = series.listIterator();
		while (iter.hasNext()) {
			U element = iter.next();
			if (element.getTimeStamp().isEqual(date)) {
				return iter.previousIndex();
			} else if(element.getTimeStamp().isBefore(date)) {
				return iter.previousIndex()-1;
			}
		}
		throw new ValueNotFoundException("Cannot find value for date '" + date + "'.");
	}
	
	@SuppressWarnings("unchecked")
	public static <T extends TimeSeries<U>, U extends MarketDataEntity<U>>
	T feasibleSubList(Class<U> clazz, T series, TimeStamp start, TimeStamp end) throws ValueNotFoundException {
		int startPos = getLastFeasiblePositionTimeStamp(clazz, series, start) + 1;
		int endPos = getFeasibleTimeStampPosition(clazz, series, end);
		return (T) series.subList(endPos, startPos);
	}
}
